Skip to content

feat: implement albums feature with create, edit, delete, and image management UI#610

Open
SiddharthJiyani wants to merge 13 commits into
AOSSIE-Org:mainfrom
SiddharthJiyani:feat/albums-ui-and-backend
Open

feat: implement albums feature with create, edit, delete, and image management UI#610
SiddharthJiyani wants to merge 13 commits into
AOSSIE-Org:mainfrom
SiddharthJiyani:feat/albums-ui-and-backend

Conversation

@SiddharthJiyani

@SiddharthJiyani SiddharthJiyani commented Nov 3, 2025

Copy link
Copy Markdown
Contributor

Description

This PR implements a complete Albums feature , including both backend API endpoints and a fully functional frontend UI.

Features Implemented

Backend

  • Improved Album CRUD operations (Create, Read, Update, Delete)
  • Album locking/password protection
  • Cover image management
  • Image management within albums (add, remove, reorder)
  • Proper error handling and validation

Frontend

  • Albums UI with album cards
  • Create album dialog with name, description, and lock option
  • Edit album dialog with ability to change lock status
  • Delete album with confirmation dialog
  • Password-protected access for locked albums
  • Add/remove images from albums
  • Set album cover image
  • Dark/Light theme support with appropriate placeholder images

Technical Details

Architecture

  • State Management: Redux with albumsSlice.ts for persistent state
  • API Integration: usePictoQuery and usePictoMutation hooks
  • UI Components: Standard ShadCN components (Dialog, Button, Input, Label, Switch, etc.)
  • User Feedback: infoDialogSlice.ts for messages and loaderSlice.ts for loading states
  • Type Safety: Full TypeScript coverage with proper interfaces

Key Changes

  1. Created Redux slice (albumsSlice.ts) with 11 actions for album state management
  2. Created memoized selectors (albumSelectors.ts) using createSelector
  3. Implemented API functions for all album operations (albums.ts)
  4. Built comprehensive album components:
    • AlbumCard.tsx - Album display with lock icon
    • CreateAlbumDialog.tsx - New album creation
    • EditAlbumDialog.tsx - Album editing with lock management
    • DeleteConfirmDialog.tsx - Delete confirmation
    • AlbumPasswordDialog.tsx - Password entry for locked albums
    • AddImagesToAlbumDialog.tsx - Image management
  5. Created album pages:
    • Album.tsx - Albums list view
    • AlbumDetail.tsx - Album detail view with images
  6. Added proper error handling with detailed backend error messages

Testing Checklist

  • Create album (locked and unlocked)
  • Edit album (change name, description, lock status)
  • Delete album with confirmation
  • Lock/unlock albums with password
  • Add/remove images from album
  • Set album cover image
  • Auto-refresh UI after operations
  • Error messages display properly
  • Dark/Light theme support
  • Responsive design on mobile and desktop

Related Issues

Tasks

  • Use standard ShadCN components to create the UI. (Refer to other components)
  • Store all the state information using Redux only. (Refer to similar files to know how to create a Redux slice and store the query results)
  • Use only usePictoQuery and usePictoMutation to handle API calling.
  • Utilize the standard infoDialogSlice.ts and loaderSlice.ts to display dialog messages and loaders, respectively.
pictopy-album-issue_.2.mp4

Summary by CodeRabbit

  • New Features

    • Albums now support locking (password protection), cover images, per-album photo counts, and a new API to set an album's cover.
  • UI

    • Full album UI: listing, detail/gallery, create, edit, delete, add-images, password prompt, album card, and empty-state components.
  • Behavior Changes

    • Album visibility uses "is_locked"; album list no longer accepts a show-hidden parameter and payloads include is_locked, cover_image_path, and image_count.

@coderabbitai

coderabbitai Bot commented Nov 3, 2025

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Replaces album visibility is_hidden with is_locked, adds cover_image_path and image_count to album schema with a migration, adds cover-image update API and helpers, and implements a full frontend albums feature set (API clients, Redux slice, components, pages, routes, and types).

Changes

Cohort / File(s) Summary
Backend DB & Schema
backend/app/database/albums.py, backend/app/schemas/album.py
Rename is_hiddenis_locked; add cover_image_path and image_count; add migration db_migrate_add_cover_image_column(); tighten SELECT columns; add db_update_album_cover_image() and db_get_image_path(); update insert/update signatures.
Backend Routes & Tests
backend/app/routes/albums.py, backend/tests/test_albums.py
Remove show_hidden param; use is_locked semantics; include cover_image_path and image_count in responses; add PUT /albums/{album_id}/cover; update tests and fixtures (mock_db_locked_album).
Frontend API & Endpoints
frontend/src/api/apiEndpoints.ts, frontend/src/api/api-functions/albums.ts, frontend/src/api/api-functions/index.ts
Add albumsEndpoints and albums client functions (get/create/update/delete, image management, set cover); re-export in API index.
Frontend State
frontend/src/features/albumsSlice.ts, frontend/src/features/albumSelectors.ts, frontend/src/app/store.ts
New Redux slice for albums (albums, selectedAlbum, albumImages) with actions; selectors including memoized counts; integrate albumsReducer into root store.
Frontend Components — Albums UI
frontend/src/components/Albums/*.tsx, frontend/src/components/EmptyStates/EmptyAlbumsState.tsx
New components: AlbumCard, CreateAlbumDialog, EditAlbumDialog, DeleteConfirmDialog, AlbumPasswordDialog, AddImagesToAlbumDialog, empty-state UI. Components use cover_image_path, is_locked, and update flows.
Frontend Pages & Routing
frontend/src/pages/Album/Album.tsx, frontend/src/pages/Album/AlbumDetail.tsx, frontend/src/routes/AppRoutes.tsx, frontend/src/constants/routes.ts
Rename/implement Albums list page, add AlbumDetail page with gallery and cover-setting, update routes and add ALBUM_DETAIL route.
Frontend Types
frontend/src/types/Album.ts
Redefined Album shape (id, name, description, is_locked, cover_image_path, image_count, timestamps) and request/prop types (Create/Update/Add/Remove/Get images, dialog props); removed legacy list interfaces.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Albums UI
    participant API as Backend API
    participant DB as Database

    User->>UI: Open CreateAlbumDialog
    UI->>API: POST /albums {name,is_locked,password}
    API->>DB: db_insert_album(...)
    DB-->>API: OK
    API-->>UI: 201 Created
    UI->>API: GET /albums
    API->>DB: db_get_all_albums()
    DB-->>API: [albums with cover_image_path,image_count]
    API-->>UI: Album list
Loading
sequenceDiagram
    participant User
    participant Detail as AlbumDetail Page
    participant UIAdd as AddImagesToAlbumDialog
    participant API as Backend API
    participant DB as Database

    User->>Detail: View album
    Detail->>API: GET /albums/{id}
    API->>DB: db_get_album(id)
    DB-->>API: album (cover_image_path,image_count)
    API-->>Detail: album data
    User->>Detail: Set cover image (choose image)
    Detail->>API: PUT /albums/{id}/cover {image_id}
    API->>DB: db_get_image_path(image_id)
    API->>DB: db_update_album_cover_image(id, path)
    DB-->>API: OK
    API-->>Detail: 200 Updated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Suggested reviewers

  • rahulharpal1603

Poem

🐇
I hopped through fields of code and locks,
Found cover frames and counted flocks.
Dialogs, cards, and an API lane,
I nibbled bugs, then hopped again—
Commit secured, I munch a carrot box.

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (88 files):

⚔️ .github/ISSUE_TEMPLATE/bug.yml (content)
⚔️ .github/ISSUE_TEMPLATE/feature.yml (content)
⚔️ .github/workflows/build-and-release.yml (content)
⚔️ .github/workflows/pr-check-build.yml (content)
⚔️ .gitignore (content)
⚔️ .pre-commit-config.yaml (content)
⚔️ CONTRIBUTING.md (content)
⚔️ backend/app/config/settings.py (content)
⚔️ backend/app/database/albums.py (content)
⚔️ backend/app/database/face_clusters.py (content)
⚔️ backend/app/database/faces.py (content)
⚔️ backend/app/database/images.py (content)
⚔️ backend/app/database/metadata.py (content)
⚔️ backend/app/logging/setup_logging.py (content)
⚔️ backend/app/models/FaceDetector.py (content)
⚔️ backend/app/routes/albums.py (content)
⚔️ backend/app/routes/images.py (content)
⚔️ backend/app/schemas/album.py (content)
⚔️ backend/app/utils/face_clusters.py (content)
⚔️ backend/main.py (content)
⚔️ backend/requirements.txt (content)
⚔️ backend/tests/test_albums.py (content)
⚔️ docs/Manual_Setup_Guide.md (content)
⚔️ docs/Script_Setup_Guide.md (content)
⚔️ docs/backend/backend_python/api.md (content)
⚔️ docs/backend/backend_python/openapi.json (content)
⚔️ docs/backend/backend_rust/api.md (content)
⚔️ docs/stylesheets/extra.css (content)
⚔️ frontend/package-lock.json (content)
⚔️ frontend/scripts/setup_env.sh (content)
⚔️ frontend/scripts/setup_win.ps1 (content)
⚔️ frontend/src-tauri/Cargo.lock (content)
⚔️ frontend/src-tauri/Cargo.toml (content)
⚔️ frontend/src-tauri/Entitlements.plist (content)
⚔️ frontend/src-tauri/capabilities/migrated.json (content)
⚔️ frontend/src-tauri/src/main.rs (content)
⚔️ frontend/src-tauri/src/services/mod.rs (content)
⚔️ frontend/src-tauri/tauri.conf.json (content)
⚔️ frontend/src/api/api-functions/index.ts (content)
⚔️ frontend/src/api/apiEndpoints.ts (content)
⚔️ frontend/src/app/store.ts (content)
⚔️ frontend/src/components/EmptyStates/EmptyGalleryState.tsx (content)
⚔️ frontend/src/components/Media/ImageCard.tsx (content)
⚔️ frontend/src/components/Media/MediaInfoPanel.tsx (content)
⚔️ frontend/src/components/Media/MediaThumbnails.tsx (content)
⚔️ frontend/src/components/Media/MediaView.tsx (content)
⚔️ frontend/src/components/Media/MediaViewControls.tsx (content)
⚔️ frontend/src/components/Media/NavigationButtons.tsx (content)
⚔️ frontend/src/components/Navigation/Sidebar/AppSidebar.tsx (content)
⚔️ frontend/src/components/OnboardingSteps/AvatarSelectionStep.tsx (content)
⚔️ frontend/src/components/OnboardingSteps/FolderSetupStep.tsx (content)
⚔️ frontend/src/components/OnboardingSteps/OnboardingStep.tsx (content)
⚔️ frontend/src/components/OnboardingSteps/ServerCheck.tsx (content)
⚔️ frontend/src/components/OnboardingSteps/ThemeSelectionStep.tsx (content)
⚔️ frontend/src/config/Backend.ts (content)
⚔️ frontend/src/constants/routes.ts (content)
⚔️ frontend/src/features/onboardingSlice.ts (content)
⚔️ frontend/src/hooks/useFolderOperations.tsx (content)
⚔️ frontend/src/hooks/useSlideshow.ts (content)
⚔️ frontend/src/layout/layout.tsx (content)
⚔️ frontend/src/main.tsx (content)
⚔️ frontend/src/pages/Album/Album.tsx (content)
⚔️ frontend/src/pages/Home/Home.tsx (content)
⚔️ frontend/src/pages/SettingsPage/components/ApplicationControlsCard.tsx (content)
⚔️ frontend/src/routes/AppRoutes.tsx (content)
⚔️ frontend/src/types/Album.ts (content)
⚔️ frontend/src/types/Media.ts (content)
⚔️ landing-page/index.html (content)
⚔️ landing-page/src/Pages/Demo/marqu.tsx (content)
⚔️ landing-page/src/Pages/FaqPage/FAQ.tsx (content)
⚔️ landing-page/src/Pages/Footer/Footer.tsx (content)
⚔️ landing-page/src/Pages/HowItWorks/HowItWorks.tsx (content)
⚔️ landing-page/src/Pages/Landing page/Home1.tsx (content)
⚔️ landing-page/src/Pages/pictopy-landing.tsx (content)
⚔️ mkdocs.yml (content)
⚔️ scripts/setup.js (content)
⚔️ scripts/setup.ps1 (content)
⚔️ scripts/setup.sh (content)
⚔️ sync-microservice/README.md (content)
⚔️ sync-microservice/app/config/settings.py (content)
⚔️ sync-microservice/app/core/lifespan.py (content)
⚔️ sync-microservice/app/logging/setup_logging.py (content)
⚔️ sync-microservice/app/routes/folders.py (content)
⚔️ sync-microservice/app/routes/health.py (content)
⚔️ sync-microservice/app/routes/watcher.py (content)
⚔️ sync-microservice/app/utils/watcher_helpers.py (content)
⚔️ sync-microservice/main.py (content)
⚔️ sync-microservice/requirements.txt (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the primary feature being implemented - a complete albums feature with UI for create, edit, delete operations and image management.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@SiddharthJiyani SiddharthJiyani force-pushed the feat/albums-ui-and-backend branch from ff90128 to afcecea Compare November 3, 2025 14:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/types/Album.ts (1)

59-92: Remove duplicate interface definitions.

PasswordPromptDialogProps is defined twice (lines 59-64 and 80-85), and AddImagesToAlbumDialogProps is also defined twice (lines 66-71 and 87-92). This creates confusion and potential maintenance issues if the definitions diverge.

Remove the duplicate definitions:

 export interface CreateAlbumDialogProps {
   isOpen: boolean;
   onClose: () => void;
   onSuccess?: () => void;
 }

 export interface PasswordPromptDialogProps {
   isOpen: boolean;
   onClose: () => void;
   onSubmit: (password: string) => void;
   albumName: string;
 }

+export interface AddImagesToAlbumDialogProps {
+  isOpen: boolean;
+  onClose: () => void;
+  albumId: string;
+  albumName: string;
+}
+
 export interface AlbumCardProps {
   album: Album;
   onClick: () => void;
   onEdit: () => void;
   onDelete: () => void;
 }
-
-export interface PasswordPromptDialogProps {
-  isOpen: boolean;
-  onClose: () => void;
-  onSubmit: (password: string) => void;
-  albumName: string;
-}
-
-export interface AddImagesToAlbumDialogProps {
-  isOpen: boolean;
-  onClose: () => void;
-  albumId: string;
-  albumName: string;
-}
🧹 Nitpick comments (4)
frontend/src/components/EmptyStates/EmptyAlbumsState.tsx (1)

3-28: Consider adding aria-hidden to decorative icons.

The component structure and implementation are excellent. The layout, styling, dark mode support, and user guidance are well-executed.

For enhanced accessibility, consider adding aria-hidden="true" to decorative icons on lines 7, 18, and 22, since they don't convey information beyond what the text already provides.

Example for line 7:

-        <BookImage className="h-16 w-16 text-gray-400" strokeWidth={1.5} />
+        <BookImage className="h-16 w-16 text-gray-400" strokeWidth={1.5} aria-hidden="true" />
frontend/src/pages/Album/AlbumDetail.tsx (2)

153-174: Backend data transformation handles schema variations defensively.

The transformation logic safely handles multiple possible backend response structures using optional chaining and defaults. However, the use of any types suggests the backend API contract could benefit from stronger typing.

Consider defining a TypeScript interface for the backend album response to eliminate the any casts and improve type safety:

interface BackendAlbum {
  album_id: string;
  album_name: string;
  description?: string;
  is_locked?: boolean;
  cover_image_path?: string;
  image_count?: number;
  created_at?: string;
  updated_at?: string;
}

176-213: Image loading works but has inefficient data fetching pattern.

The implementation fetches all images from the library, then filters by album image IDs. While functional, this becomes inefficient as the image library grows.

Consider updating the backend getAlbumImages endpoint to return full image objects rather than just IDs, eliminating the need to fetch and filter the entire image library:

// Instead of fetching all images
const { data: allImagesData } = usePictoQuery({
  queryKey: ['images'],
  queryFn: () => fetchAllImages(),
  enabled: !!albumId && !!album,
});

// Backend could return full image objects directly
const { data: imagesData } = usePictoQuery({
  queryKey: ['album-images', albumId],
  queryFn: () => getAlbumImages(albumId!, password ? { password } : undefined),
  enabled: !!albumId && !!album,
});
frontend/src/features/albumsSlice.ts (1)

56-70: Consider syncing image_count from server.

The local image_count updates (increment on add, decrement on remove) are correct, but could drift from the server if operations fail or are retried. Consider refreshing the selected album from the server after image operations complete to ensure consistency.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe2f98d and afcecea.

⛔ Files ignored due to path filters (2)
  • frontend/public/placeholder-album-light.svg is excluded by !**/*.svg
  • frontend/public/placeholder-album.svg is excluded by !**/*.svg
📒 Files selected for processing (21)
  • backend/app/database/albums.py (10 hunks)
  • backend/app/routes/albums.py (10 hunks)
  • backend/app/schemas/album.py (3 hunks)
  • frontend/src/api/api-functions/albums.ts (1 hunks)
  • frontend/src/api/api-functions/index.ts (1 hunks)
  • frontend/src/api/apiEndpoints.ts (1 hunks)
  • frontend/src/app/store.ts (2 hunks)
  • frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (1 hunks)
  • frontend/src/components/Albums/AlbumCard.tsx (1 hunks)
  • frontend/src/components/Albums/AlbumPasswordDialog.tsx (1 hunks)
  • frontend/src/components/Albums/CreateAlbumDialog.tsx (1 hunks)
  • frontend/src/components/Albums/DeleteConfirmDialog.tsx (1 hunks)
  • frontend/src/components/Albums/EditAlbumDialog.tsx (1 hunks)
  • frontend/src/components/EmptyStates/EmptyAlbumsState.tsx (1 hunks)
  • frontend/src/constants/routes.ts (1 hunks)
  • frontend/src/features/albumSelectors.ts (1 hunks)
  • frontend/src/features/albumsSlice.ts (1 hunks)
  • frontend/src/pages/Album/Album.tsx (1 hunks)
  • frontend/src/pages/Album/AlbumDetail.tsx (1 hunks)
  • frontend/src/routes/AppRoutes.tsx (2 hunks)
  • frontend/src/types/Album.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (15)
frontend/src/components/Albums/AlbumCard.tsx (2)
frontend/src/types/Album.ts (1)
  • AlbumCardProps (73-78)
frontend/src/contexts/ThemeContext.tsx (1)
  • useTheme (66-73)
frontend/src/components/Albums/CreateAlbumDialog.tsx (5)
frontend/src/types/Album.ts (1)
  • CreateAlbumDialogProps (53-57)
frontend/src/hooks/useQueryExtension.ts (1)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/albums.ts (1)
  • createAlbum (37-45)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
frontend/src/components/Albums/AlbumPasswordDialog.tsx (1)
frontend/src/types/Album.ts (2)
  • PasswordPromptDialogProps (59-64)
  • PasswordPromptDialogProps (80-85)
backend/app/schemas/album.py (1)
frontend/src/types/Album.ts (1)
  • UpdateAlbumRequest (26-32)
frontend/src/features/albumsSlice.ts (2)
frontend/src/types/Album.ts (1)
  • Album (1-10)
frontend/src/types/Media.ts (1)
  • Image (13-22)
frontend/src/components/Albums/EditAlbumDialog.tsx (5)
frontend/src/types/Album.ts (1)
  • EditAlbumDialogProps (46-51)
frontend/src/hooks/useQueryExtension.ts (1)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/albums.ts (1)
  • updateAlbum (52-61)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
frontend/src/api/api-functions/albums.ts (4)
frontend/src/types/API.ts (1)
  • APIResponse (1-8)
frontend/src/api/axiosConfig.ts (1)
  • apiClient (5-12)
frontend/src/api/apiEndpoints.ts (1)
  • albumsEndpoints (33-46)
frontend/src/types/Album.ts (5)
  • CreateAlbumRequest (19-24)
  • UpdateAlbumRequest (26-32)
  • AddImagesToAlbumRequest (34-36)
  • GetAlbumImagesRequest (38-40)
  • RemoveImagesFromAlbumRequest (42-44)
backend/app/routes/albums.py (2)
backend/app/schemas/album.py (5)
  • SetCoverImageRequest (55-56)
  • GetAlbumsResponse (64-66)
  • Album (6-12)
  • SuccessResponse (84-86)
  • ErrorResponse (89-92)
backend/app/database/albums.py (5)
  • db_update_album_cover_image (221-232)
  • verify_album_password (299-311)
  • db_get_all_albums (107-119)
  • db_get_album_images (235-245)
  • db_get_album (136-147)
frontend/src/routes/AppRoutes.tsx (2)
frontend/src/constants/routes.ts (1)
  • ROUTES (1-12)
frontend/src/pages/Album/AlbumDetail.tsx (1)
  • AlbumDetail (43-437)
frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (7)
frontend/src/types/Album.ts (2)
  • AddImagesToAlbumDialogProps (66-71)
  • AddImagesToAlbumDialogProps (87-92)
frontend/src/types/Media.ts (1)
  • Image (13-22)
frontend/src/hooks/useQueryExtension.ts (2)
  • usePictoQuery (80-108)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/images.ts (1)
  • fetchAllImages (5-14)
frontend/src/api/api-functions/albums.ts (1)
  • addImagesToAlbum (79-88)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
frontend/src/components/Albums/DeleteConfirmDialog.tsx (1)
frontend/src/components/ui/dialog.tsx (6)
  • Dialog (133-133)
  • DialogContent (135-135)
  • DialogHeader (138-138)
  • DialogTitle (141-141)
  • DialogDescription (136-136)
  • DialogFooter (137-137)
frontend/src/features/albumSelectors.ts (1)
frontend/src/app/store.ts (1)
  • RootState (24-24)
frontend/src/types/Album.ts (1)
backend/app/schemas/album.py (4)
  • CreateAlbumRequest (20-30)
  • UpdateAlbumRequest (33-44)
  • GetAlbumImagesRequest (47-48)
  • Album (6-12)
frontend/src/pages/Album/AlbumDetail.tsx (13)
frontend/src/features/albumSelectors.ts (2)
  • selectSelectedAlbum (7-8)
  • selectAlbumImages (10-10)
frontend/src/features/imageSelectors.ts (1)
  • selectIsImageViewOpen (13-16)
frontend/src/hooks/useQueryExtension.ts (2)
  • usePictoQuery (80-108)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/albums.ts (4)
  • getAlbumById (26-31)
  • getAlbumImages (95-104)
  • removeMultipleImagesFromAlbum (126-135)
  • setAlbumCoverImage (142-151)
frontend/src/api/api-functions/images.ts (1)
  • fetchAllImages (5-14)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
frontend/src/types/Album.ts (1)
  • Album (1-10)
frontend/src/features/albumsSlice.ts (3)
  • setSelectedAlbum (48-50)
  • setAlbumImages (52-54)
  • clearAlbumImages (72-74)
frontend/src/features/imageSlice.ts (3)
  • setImages (18-20)
  • clearImages (39-42)
  • setCurrentViewIndex (22-34)
frontend/src/components/Media/ImageCard.tsx (1)
  • ImageCard (19-109)
frontend/src/components/Media/MediaView.tsx (1)
  • MediaView (23-214)
frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (1)
  • AddImagesToAlbumDialog (23-210)
frontend/src/pages/Album/Album.tsx (7)
frontend/src/features/albumSelectors.ts (1)
  • selectAlbums (5-5)
frontend/src/features/albumsSlice.ts (2)
  • removeAlbum (38-46)
  • setAlbums (21-23)
frontend/src/types/Album.ts (1)
  • Album (1-10)
frontend/src/hooks/useQueryExtension.ts (2)
  • usePictoQuery (80-108)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/albums.ts (2)
  • getAllAlbums (15-20)
  • deleteAlbum (67-72)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
🪛 GitHub Actions: PR Check
backend/app/routes/albums.py

[error] 1-1: API response for /albums endpoints is missing the 'is_hidden' field in album objects. Tests expect this field in the JSON response but it is not present.


[error] 1-1: db_get_all_albums() is being called without a boolean argument, while tests expect it to be called with a 'show_hidden' flag (True/False). This mismatch causes test failures.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Tauri Build Check (windows-latest)
🔇 Additional comments (11)
frontend/src/components/EmptyStates/EmptyAlbumsState.tsx (1)

1-2: LGTM!

The icon imports are appropriate for the empty state component.

frontend/src/api/apiEndpoints.ts (1)

33-46: LGTM! Clean endpoint definitions.

The album endpoints follow the established pattern and RESTful conventions. The implementation is consistent with existing endpoint groups in the file.

frontend/src/app/store.ts (1)

9-20: LGTM! Proper Redux integration.

The albums reducer is correctly imported and integrated into the store configuration following the established pattern.

frontend/src/components/Albums/EditAlbumDialog.tsx (2)

92-112: Validation logic handles lock state transitions correctly.

The validation properly distinguishes between:

  • Locking an unlocked album (requires new password)
  • Modifying a locked album (requires current password)
  • Changing password on locked album (current password required, new password optional)

22-301: Well-implemented edit dialog with proper password state management.

The component correctly handles the complex logic for editing locked/unlocked albums, including:

  • Appropriate validation for different lock states
  • Clear UI feedback with contextual labels
  • Proper cleanup on close
frontend/src/components/Albums/AlbumCard.tsx (2)

26-55: Robust image handling with proper error fallback.

The component correctly:

  • Selects theme-appropriate placeholders
  • Uses Tauri's convertFileSrc for local file paths
  • Prevents infinite error loops by setting img.onerror = null

92-103: Clean presentation with proper text handling.

The info section correctly:

  • Truncates long album names
  • Clamps description to 2 lines
  • Pluralizes "photo/photos" appropriately
frontend/src/pages/Album/AlbumDetail.tsx (2)

215-221: Proper cleanup prevents state leaks.

The cleanup effect correctly clears all album-related state on unmount, preventing stale data from persisting.


43-437: Comprehensive album detail page with good UX patterns.

The component effectively handles:

  • Password-protected album access
  • Multiple viewing modes (normal, selection, media viewer)
  • Bulk operations with clear visual feedback
  • Context menus for per-image actions
  • Proper state cleanup on unmount
frontend/src/api/api-functions/albums.ts (1)

1-151: LGTM! Well-structured API layer.

The API functions are well-documented, properly typed, and use appropriate HTTP methods. The consistent error handling pattern and clear separation of concerns make this module easy to maintain.

backend/app/database/albums.py (1)

65-82: LGTM! Well-implemented migration.

The migration function properly checks for column existence before altering the table, preventing errors on repeated runs. Good defensive programming.

Comment thread backend/app/database/albums.py Outdated
Comment thread backend/app/routes/albums.py
Comment thread backend/app/routes/albums.py Outdated
Comment thread backend/app/routes/albums.py
Comment thread backend/app/schemas/album.py
Comment thread frontend/src/components/Albums/AddImagesToAlbumDialog.tsx
Comment thread frontend/src/components/Albums/AlbumPasswordDialog.tsx
Comment thread frontend/src/features/albumSelectors.ts Outdated
Comment thread frontend/src/types/Album.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/tests/test_albums.py (2)

467-488: Fix inconsistent mock return tuple shape.

Line 477 uses tuple(mock_db_album.values()) which creates a 5-element tuple from the fixture dict. However, other tests throughout this file explicitly construct 6-element tuples that include image_count as the last element. This inconsistency could cause test failures if db_get_album expects to return 6 elements.

Apply this diff to use a consistent 6-element tuple:

-        with patch("app.routes.albums.db_get_album") as mock_get, patch(
-            "app.routes.albums.db_remove_images_from_album"
-        ) as mock_remove_bulk:
-            mock_get.return_value = tuple(mock_db_album.values())
+        album_tuple = (
+            mock_db_album["album_id"],
+            mock_db_album["album_name"],
+            mock_db_album["description"],
+            int(mock_db_album["is_locked"]),
+            mock_db_album["password_hash"],
+            0,  # image_count
+        )
+
+        with patch("app.routes.albums.db_get_album") as mock_get, patch(
+            "app.routes.albums.db_remove_images_from_album"
+        ) as mock_remove_bulk:
+            mock_get.return_value = album_tuple
             response = client.request(
                 "DELETE", f"/albums/{album_id}/images", json=image_ids_to_remove
             )

99-105: Add missing password_hash field to the 6-element tuple for db_get_album_by_name mock.

The db_get_album_by_name function's SQL query selects 6 fields (album_id, album_name, description, is_locked, password_hash, cover_image_path), but this mock returns only 5 elements. Update the mock to include the password_hash field as the 5th element:

mock_get_by_name.return_value = (
    "existing-id",
    "Existing Album",
    "desc",
    0,
    None,  # password_hash
    None,  # cover_image_path
)
🧹 Nitpick comments (1)
backend/tests/test_albums.py (1)

149-185: Consider renaming test function for consistency.

The test function name still contains "hidden" (test_get_all_albums_include_hidden), while the docstring and implementation have been updated to use "locked" terminology. For consistency, consider renaming this to test_get_all_albums_include_locked.

-    def test_get_all_albums_include_hidden(self, mock_db_album, mock_db_locked_album):
+    def test_get_all_albums_include_locked(self, mock_db_album, mock_db_locked_album):
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between afcecea and 5841ada.

📒 Files selected for processing (1)
  • backend/tests/test_albums.py (14 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
backend/tests/test_albums.py (1)
backend/tests/test_folders.py (1)
  • client (76-78)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Tauri Build Check (ubuntu-22.04)
  • GitHub Check: Tauri Build Check (macos-latest, --target aarch64-apple-darwin)
  • GitHub Check: Tauri Build Check (windows-latest)
  • GitHub Check: Backend Tests
🔇 Additional comments (3)
backend/tests/test_albums.py (3)

23-42: LGTM! Fixture updates are correct.

The fixtures have been correctly updated to reflect the migration from is_hidden to is_locked, and the naming change from mock_db_hidden_album to mock_db_locked_album is appropriate.


53-352: Test updates for CRUD operations look good.

The test methods in TestAlbumRoutes have been appropriately updated to reflect the migration from is_hidden to is_locked. The parametrized tests provide good coverage of different scenarios (public/locked albums, password validation), and most mock return tuples consistently use the 6-element format with cover_image_path and image_count fields.


355-466: Test updates for image management operations look good.

The test methods in TestAlbumImageManagement correctly use the updated 6-element tuple format for mock album data, ensuring consistency with the new album schema that includes is_locked, cover_image_path, and image_count fields.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
backend/app/database/albums.py (1)

107-118: LGTM! Past issue with unused parameter resolved.

The removal of the show_hidden/show_locked parameter addresses the previous review comment about an ignored parameter. The function now has clear, unambiguous behavior—it always returns all albums.

backend/app/routes/albums.py (2)

128-182: LGTM! Update endpoint correctly refactored.

The endpoint correctly uses is_locked throughout, and there's no duplicate route decorator issue (which was mentioned in a previous review). The password validation logic for locked albums is properly maintained.


376-430: LGTM! Cover image endpoint is well-implemented.

The new endpoint has comprehensive validation:

  1. Album existence check
  2. Image membership verification (image must be in the album)
  3. Image existence validation

The use of db_get_image_path maintains proper architectural separation (addressing a previous review comment about inline database queries).

🧹 Nitpick comments (4)
frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (2)

34-44: Remove redundant state duplication.

The allImages state (line 32) unnecessarily duplicates imagesData.data. This adds complexity, extra re-renders, and a type assertion that could be unsafe.

Apply this diff to use imagesData.data directly:

-  const [allImages, setAllImages] = useState<Image[]>([]);

   const { data: imagesData, isLoading } = usePictoQuery({
     queryKey: ['images'],
     queryFn: () => fetchAllImages(),
     enabled: isOpen,
   });

-  useEffect(() => {
-    if (imagesData?.data) {
-      setAllImages(imagesData.data as Image[]);
-    }
-  }, [imagesData]);

Then update line 104 to use imagesData?.data directly:

-  const filteredImages = allImages.filter((image) => {
+  const filteredImages = (imagesData?.data as Image[] || []).filter((image) => {

46-96: Simplify by removing manual loader dispatches.

The component uses both manual loader dispatches (showLoader/hideLoader at lines 50, 61, 94) and the isPending flag from the mutation. This is redundant—isPending already provides the loading state needed for UI feedback.

Remove the manual loader dispatches and rely solely on isPending:

   const handleSubmit = () => {
     if (selectedImages.size === 0) {
       dispatch(
         showInfoDialog({
           title: 'No Images Selected',
           message: 'Please select at least one image to add to the album.',
           variant: 'info',
         }),
       );
       return;
     }

-    dispatch(showLoader('Adding images to album...'));
     addImagesMutate({ image_ids: Array.from(selectedImages) });
   };

And remove hideLoader calls from the mutation callbacks (lines 50, 61):

   const { mutate: addImagesMutate, isPending } = usePictoMutation({
     mutationFn: (data: { image_ids: string[] }) =>
       addImagesToAlbum(albumId, data),
     onSuccess: () => {
-      dispatch(hideLoader());
       dispatch(
         showInfoDialog({
           title: 'Success',
           message: `Added ${selectedImages.size} image${selectedImages.size > 1 ? 's' : ''} to album!`,
           variant: 'info',
         }),
       );
       handleClose();
     },
     onError: (error: any) => {
-      dispatch(hideLoader());
       dispatch(
         showInfoDialog({
           title: 'Error',
           message: error?.message || 'Failed to add images. Please try again.',
           variant: 'error',
         }),
       );
     },
   });

The button states at lines 200 and 206 already use isPending correctly.

backend/app/database/albums.py (1)

220-322: Consider using the context manager consistently.

While not critical, there's an inconsistency in connection handling: db_update_album_cover_image and db_get_image_path use manual connection management, while some other functions (e.g., db_delete_album, db_add_images_to_album) use the get_db_connection() context manager defined at the top of the file. The context manager provides retry logic and better error handling.

Consider refactoring these functions to use the context manager:

 def db_update_album_cover_image(album_id: str, cover_image_path: str):
     """Update the cover image path for an album"""
-    conn = sqlite3.connect(DATABASE_PATH)
-    cursor = conn.cursor()
-    try:
+    with get_db_connection() as conn:
+        cursor = conn.cursor()
         cursor.execute(
             "UPDATE albums SET cover_image_path = ? WHERE album_id = ?",
             (cover_image_path, album_id),
         )
-        conn.commit()
-    finally:
-        conn.close()

Apply similar changes to db_get_image_path and other functions using manual connection management.

backend/app/routes/albums.py (1)

43-45: Consider optimizing image count queries.

The current implementation performs N+1 queries: one to fetch all albums, then one per album to count images. For large album collections, this could be slow.

Consider adding a database function that returns albums with image counts in a single query:

def db_get_all_albums_with_counts():
    """Get all albums with their image counts in a single query"""
    conn = sqlite3.connect(DATABASE_PATH)
    cursor = conn.cursor()
    try:
        cursor.execute(
            """
            SELECT a.album_id, a.album_name, a.description, a.is_locked, 
                   a.password_hash, a.cover_image_path, COUNT(ai.image_id) as image_count
            FROM albums a
            LEFT JOIN album_images ai ON a.album_id = ai.album_id
            GROUP BY a.album_id
            """
        )
        return cursor.fetchall()
    finally:
        conn.close()

This reduces the operation from O(n) queries to O(1).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5841ada and 67e9937.

📒 Files selected for processing (8)
  • backend/app/database/albums.py (11 hunks)
  • backend/app/routes/albums.py (9 hunks)
  • backend/app/schemas/album.py (3 hunks)
  • backend/tests/test_albums.py (14 hunks)
  • frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (1 hunks)
  • frontend/src/components/Albums/AlbumPasswordDialog.tsx (1 hunks)
  • frontend/src/features/albumSelectors.ts (1 hunks)
  • frontend/src/types/Album.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • frontend/src/features/albumSelectors.ts
  • frontend/src/components/Albums/AlbumPasswordDialog.tsx
  • backend/app/schemas/album.py
  • frontend/src/types/Album.ts
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (7)
frontend/src/types/Album.ts (1)
  • AddImagesToAlbumDialogProps (66-71)
frontend/src/types/Media.ts (1)
  • Image (13-22)
frontend/src/hooks/useQueryExtension.ts (2)
  • usePictoQuery (80-108)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/images.ts (1)
  • fetchAllImages (5-14)
frontend/src/api/api-functions/albums.ts (1)
  • addImagesToAlbum (79-88)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
backend/app/routes/albums.py (2)
backend/app/schemas/album.py (5)
  • SetCoverImageRequest (60-61)
  • GetAlbumsResponse (69-71)
  • Album (6-12)
  • ErrorResponse (94-97)
  • SuccessResponse (89-91)
backend/app/database/albums.py (5)
  • db_update_album_cover_image (220-231)
  • db_get_image_path (313-322)
  • db_get_all_albums (107-118)
  • db_get_album_images (234-244)
  • db_get_album (135-146)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Tauri Build Check (windows-latest)
🔇 Additional comments (18)
frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (6)

1-21: LGTM!

Imports are well-organized and all dependencies are properly declared.


23-32: LGTM!

Component props and state initialization are correct. Using a Set for selectedImages is an efficient choice for fast lookups and toggles.


72-102: LGTM!

Handler functions are correctly implemented:

  • handleImageToggle properly creates a new Set for immutability
  • handleSubmit validates selection before proceeding
  • handleClose properly resets all local state before closing

104-108: LGTM!

Filtering logic is correct with proper handling of edge cases (undefined from .pop() via || ''). Case-insensitive search provides good UX.


161-170: Past review feedback addressed—theme-aware placeholders implemented correctly.

The previous review comment about using non-existent /placeholder.svg has been resolved. The current implementation correctly uses theme-aware placeholders (/placeholder-album.svg for dark mode, /placeholder-album-light.svg for light mode) via window.matchMedia, aligning with the project's existing assets.


110-215: LGTM!

UI structure and rendering logic are well-implemented:

  • Proper loading, empty, and error states
  • Accessible search with icon
  • Responsive grid layout with visual selection feedback
  • Clear user feedback with selected count and dynamic button text
  • Buttons correctly disabled based on pending state and selection
backend/tests/test_albums.py (4)

23-42: LGTM! Fixture renaming aligns with schema changes.

The fixture renaming from mock_db_hidden_album to mock_db_locked_album and the field update from is_hidden to is_locked correctly reflect the backend schema changes.


53-68: LGTM! Test parameterization covers locked and unlocked scenarios.

The test cases correctly exercise both is_locked=False (public) and is_locked=True (password-protected) album creation flows.


119-171: LGTM! Mock return data correctly reflects database schema changes.

The addition of cover_image_path and image_count placeholders to mock return tuples aligns with the updated database layer, and all is_hidden references have been correctly replaced with is_locked.


329-353: LGTM! Test expectations correctly updated for schema changes.

The test correctly constructs album tuples with int(mock_db_album["is_locked"]) for SQLite compatibility and includes the image_count placeholder, consistent with the database layer changes.

backend/app/database/albums.py (4)

42-62: LGTM! Album table schema correctly updated.

The schema now includes is_locked, password_hash, and cover_image_path fields, aligning with the feature requirements for locked albums and cover image management.


149-211: LGTM! Parameter renaming and SQL updates are consistent.

The renaming of is_hidden to is_locked is correctly applied in both db_insert_album and db_update_album, with proper boolean-to-integer conversion for SQLite compatibility.


220-231: LGTM! Cover image update function is straightforward.

The db_update_album_cover_image function correctly updates the cover image path for an album.


313-322: LGTM! Image path retrieval function is correct.

The db_get_image_path function appropriately queries the images table to retrieve the file path for a given image ID.

backend/app/routes/albums.py (4)

1-31: LGTM! Imports correctly updated for new functionality.

The addition of SetCoverImageRequest, db_update_album_cover_image, and db_get_image_path supports the new cover image endpoint and addresses the previous inline database query issue.


36-57: LGTM! Function correctly updated for schema changes.

The removal of the parameter and the addition of image count and cover image path support align with the backend changes. The safe indexing album[5] if len(album) > 5 else None handles potential schema migration cases gracefully.


91-125: LGTM! Album detail endpoint correctly updated.

The endpoint properly incorporates image count calculation and cover image path, with consistent error handling and safe field access.


211-267: LGTM! Image retrieval endpoint correctly updated.

The endpoint properly checks is_locked status and requires password authentication for locked albums, with clear error messages.

Comment thread backend/app/database/albums.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
frontend/src/components/Albums/EditAlbumDialog.tsx (1)

38-40: Prefer explicit typing over any for type safety.

The mutation function parameter data is typed as any, which bypasses TypeScript's type checking. Consider using the UpdateAlbumRequest type instead.

Apply this diff to improve type safety:

-  const { mutate: updateAlbumMutate, isPending } = usePictoMutation({
-    mutationFn: ({ albumId, data }: { albumId: string; data: any }) =>
-      updateAlbum(albumId, data),
+  const { mutate: updateAlbumMutate, isPending } = usePictoMutation({
+    mutationFn: ({ albumId, data }: { albumId: string; data: UpdateAlbumRequest }) =>
+      updateAlbum(albumId, data),

You'll also need to import the type:

import { UpdateAlbumRequest } from '@/types/Album';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67e9937 and 634140f.

📒 Files selected for processing (3)
  • frontend/src/components/Albums/CreateAlbumDialog.tsx (1 hunks)
  • frontend/src/components/Albums/EditAlbumDialog.tsx (1 hunks)
  • frontend/src/features/albumSelectors.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/components/Albums/CreateAlbumDialog.tsx
  • frontend/src/features/albumSelectors.ts
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/components/Albums/EditAlbumDialog.tsx (5)
frontend/src/types/Album.ts (1)
  • EditAlbumDialogProps (46-51)
frontend/src/hooks/useQueryExtension.ts (1)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/albums.ts (1)
  • updateAlbum (52-61)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Tauri Build Check (ubuntu-22.04)
  • GitHub Check: Tauri Build Check (windows-latest)
  • GitHub Check: Tauri Build Check (macos-latest, --target aarch64-apple-darwin)
  • GitHub Check: Backend Tests
🔇 Additional comments (3)
frontend/src/components/Albums/EditAlbumDialog.tsx (3)

80-90: LGTM!

The form population logic correctly pre-fills the form fields from the album prop while resetting password fields to empty strings for security. The effect dependency on [album] is appropriate.


92-112: LGTM!

The validation logic correctly handles all scenarios:

  • Requires name field
  • Requires current password when modifying a locked album
  • Requires new password when locking a previously unlocked album

The use of optional chaining (album?.is_locked) safely handles null cases.


205-281: LGTM!

The password fields logic is well-implemented:

  • Current password is shown and required only for albums that were originally locked
  • New password field appears when the lock toggle is enabled
  • Labels and placeholders are contextual and user-friendly
  • Required indicators correctly match the validation rules

The UX clearly communicates when passwords are optional vs. required.

Comment thread frontend/src/components/Albums/EditAlbumDialog.tsx
Comment thread frontend/src/pages/Album/Album.tsx Outdated
Comment on lines +60 to +82
onSuccess: (_, albumId) => {
dispatch(hideLoader());
dispatch(removeAlbum(albumId as string));
dispatch(
showInfoDialog({
title: 'Success',
message: 'Album deleted successfully!',
variant: 'info',
}),
);
},
onError: (error: any) => {
dispatch(hideLoader());
dispatch(
showInfoDialog({
title: 'Error',
message:
error?.message || 'Failed to delete album. Please try again.',
variant: 'error',
}),
);
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refer this file: frontend/src/hooks/useUserPreferences.tsx

Utilize the useMutationFeedback function to handle the post-API call logic.

Make such change everywhere we are making album API calls.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copilot AI review requested due to automatic review settings February 15, 2026 13:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a complete Albums feature for the Pictopy application, adding both backend API endpoints and a fully functional frontend UI. The implementation allows users to create, edit, delete albums, manage album images, set cover images, and protect albums with passwords.

Changes:

  • Implemented complete album CRUD operations with locking/password protection
  • Built comprehensive frontend UI with dialogs for album management and image selection
  • Added Redux state management with albumsSlice and memoized selectors
  • Updated backend schema from is_hidden to is_locked with cover image support

Reviewed changes

Copilot reviewed 22 out of 24 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
frontend/src/types/Album.ts Updated type definitions replacing is_hidden with is_locked, added cover_image_path and image_count
frontend/src/routes/AppRoutes.tsx Added Album and AlbumDetail route components
frontend/src/pages/Album/Album.tsx Main albums list page with create, edit, delete, and password dialog functionality
frontend/src/pages/Album/AlbumDetail.tsx Album detail page showing images with add/remove and cover image selection
frontend/src/features/albumsSlice.ts Redux slice for albums state management with 11 actions
frontend/src/features/albumSelectors.ts Memoized selectors for album state
frontend/src/constants/routes.ts Added ALBUM_DETAIL route constant
frontend/src/components/EmptyStates/EmptyAlbumsState.tsx Empty state component for albums list
frontend/src/components/Albums/EditAlbumDialog.tsx Dialog for editing album with lock management
frontend/src/components/Albums/DeleteConfirmDialog.tsx Confirmation dialog for album deletion
frontend/src/components/Albums/CreateAlbumDialog.tsx Dialog for creating new albums
frontend/src/components/Albums/AlbumPasswordDialog.tsx Password prompt for locked albums
frontend/src/components/Albums/AlbumCard.tsx Album card component with cover image and lock icon
frontend/src/components/Albums/AddImagesToAlbumDialog.tsx Dialog for selecting and adding images to album
frontend/src/app/store.ts Integrated albums reducer into Redux store
frontend/src/api/apiEndpoints.ts Added album API endpoints configuration
frontend/src/api/api-functions/index.ts Exported album API functions
frontend/src/api/api-functions/albums.ts Comprehensive album API functions for all operations
frontend/public/placeholder-album.svg Dark theme placeholder for albums without cover
frontend/public/placeholder-album-light.svg Light theme placeholder for albums without cover
backend/tests/test_albums.py Updated tests to use is_locked instead of is_hidden
backend/app/schemas/album.py Updated schemas with is_locked, cover_image_path, and image_count
backend/app/routes/albums.py Updated routes with is_locked semantics and cover image endpoint
backend/app/database/albums.py Updated database functions and added migration function for cover_image_path

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 42 to 56
for album in albums:
# Get image count for each album
image_ids = db_get_album_images(album[0])
image_count = len(image_ids)

album_list.append(
Album(
album_id=album[0],
album_name=album[1],
description=album[2] or "",
is_hidden=bool(album[3]),
is_locked=bool(album[3]),
cover_image_path=album[5] if len(album) > 5 else None,
image_count=image_count,
)
)

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The get_albums endpoint calls db_get_album_images() for every album in a loop (line 44), which results in N+1 database queries. For users with many albums, this could cause significant performance degradation. Consider optimizing this by either: (1) adding a database query that counts images per album in a single query using a JOIN or GROUP BY, or (2) caching the image counts in the albums table itself and updating it when images are added/removed.

Copilot uses AI. Check for mistakes.
Comment thread backend/app/database/albums.py Outdated
Comment on lines +65 to +82
def db_migrate_add_cover_image_column() -> None:
"""Add cover_image_path column to existing albums table if it doesn't exist"""
conn = None
try:
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()

# Check if column exists
cursor.execute("PRAGMA table_info(albums)")
columns = [column[1] for column in cursor.fetchall()]

if "cover_image_path" not in columns:
cursor.execute("ALTER TABLE albums ADD COLUMN cover_image_path TEXT")
conn.commit()
print("Added cover_image_path column to albums table")
finally:
if conn is not None:
conn.close()

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration function db_migrate_add_cover_image_column is defined but there's no evidence it's being called during application startup in the modified code. This migration is necessary for existing databases that don't have the cover_image_path column. For backward compatibility with existing databases, this function should be called after table creation but before the application starts using album functionality.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +21

export const EditAlbumDialog: React.FC<EditAlbumDialogProps> = ({
album,

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The imports showInfoDialog, showLoader, and hideLoader are imported but never used in this component. The useMutationFeedback hook already handles loading and info dialog display. Remove these unused imports to keep the code clean.

Copilot uses AI. Check for mistakes.
Comment on lines +150 to +163
Protect your album with a password
</p>
</div>
<Switch
id="locked"
checked={formData.is_locked}
onCheckedChange={(checked) =>
setFormData({ ...formData, is_locked: checked })
}
/>
</div>

{/* Password Field (shown only if locked) */}
{formData.is_locked && (

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The htmlFor attribute on the Label references "hidden" but the Switch has id="locked". This mismatch breaks the label-input association. Change the label's htmlFor from "hidden" to "locked" to match the Switch's id attribute.

Copilot uses AI. Check for mistakes.

const handlePasswordSubmit = (password: string) => {
if (albumToAccess) {
navigate(`/albums/${albumToAccess.id}`, { state: { password } });

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing the album password through navigation state (line 117) may expose it in browser history or debugging tools. Consider implementing a more secure approach such as storing the password in memory using a ref or session storage with appropriate cleanup, or re-prompting for the password when needed on the detail page. The current approach could allow the password to be inspected through browser developer tools.

Suggested change
navigate(`/albums/${albumToAccess.id}`, { state: { password } });
try {
sessionStorage.setItem(`album_password_${albumToAccess.id}`, password);
} catch (e) {
// If sessionStorage is unavailable, proceed without storing the password.
}
navigate(`/albums/${albumToAccess.id}`);

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +104
const {
data: albumsData,
successData,
isLoading,
isSuccess,
isError,
refetch,
} = usePictoQuery({
queryKey: ['albums'],
queryFn: () => getAllAlbums(),
});

const deleteAlbumMutation = usePictoMutation({
mutationFn: deleteAlbum,
});

useMutationFeedback(deleteAlbumMutation, {
loadingMessage: 'Deleting album...',
successTitle: 'Success',
successMessage: 'Album deleted successfully!',
errorTitle: 'Error',
errorMessage: 'Failed to delete album. Please try again.',
onSuccess: () => {
if (albumToDelete) {
dispatch(removeAlbum(albumToDelete.id));
}
},
});

useEffect(() => {
if (isLoading) {
dispatch(showLoader('Loading albums...'));
} else if (isError) {
dispatch(hideLoader());
dispatch(
showInfoDialog({
title: 'Error',
message: 'Failed to load albums. Please try again later.',
variant: 'error',
}),
);
} else if (isSuccess && albumsData) {
const responseData = albumsData as any;
const backendAlbums = (responseData?.albums || []) as any[];
const albumsList = backendAlbums.map((album: any) => ({
id: album.album_id,
name: album.album_name,
description: album.description || '',
is_locked: Boolean(album.is_locked),
cover_image_path: album.cover_image_path,
image_count: album.image_count || 0,
created_at: album.created_at || new Date().toISOString(),
updated_at: album.updated_at || new Date().toISOString(),
})) as Album[];
dispatch(setAlbums(albumsList));
dispatch(hideLoader());
}
}, [albumsData, successData, isSuccess, isError, isLoading, dispatch]);

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The successData variable is destructured from the query result but is never used in the component. It's only included in the useEffect dependency array. Consider removing it from the destructuring assignment and the dependency array since albumsData alone provides all the necessary data for the effect.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +21

export const CreateAlbumDialog: React.FC<CreateAlbumDialogProps> = ({
isOpen,

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The imports showInfoDialog, showLoader, and hideLoader are imported but never used in this component. The useMutationFeedback hook already handles loading and info dialog display. Remove these unused imports to keep the code clean.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +18
import { showInfoDialog } from '@/features/infoDialogSlice';
import { useMutationFeedback } from '@/hooks/useMutationFeedback';

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The imports showInfoDialog, showLoader, and hideLoader are imported but never used in this component. The useMutationFeedback hook already handles loading and info dialog display for the mutations, while showInfoDialog for the empty selection case is the only used import. Remove showLoader and hideLoader imports.

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +160
const img = e.target as HTMLImageElement;
img.onerror = null;
const placeholder = window.matchMedia(
'(prefers-color-scheme: dark)',
).matches
? '/placeholder-album.svg'
: '/placeholder-album-light.svg';
img.src = placeholder;
}}
/>

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The placeholder image selection uses window.matchMedia('(prefers-color-scheme: dark)') to detect the theme, which checks the system preference rather than the app's theme state. This is inconsistent with AlbumCard.tsx which uses the useTheme hook. For consistent behavior, use useTheme hook from @/contexts/ThemeContext instead of window.matchMedia. This ensures the placeholder matches the user's selected theme in the app, not just the system preference.

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@frontend/src/components/Albums/AddImagesToAlbumDialog.tsx`:
- Line 18: Remove the unused imports hideLoader and showLoader from the top of
AddImagesToAlbumDialog.tsx to resolve TS6192 and ESLint errors; locate the
import line that currently imports hideLoader and showLoader and delete them so
only the actually used symbols (e.g., other imports and the useMutationFeedback
hook) remain, ensuring the file no longer references those unused symbols.

In `@frontend/src/components/Albums/CreateAlbumDialog.tsx`:
- Around line 19-20: Remove the unused imports and variable in
CreateAlbumDialog: delete the imported symbols showInfoDialog, showLoader,
hideLoader and remove the unused dispatch variable (from useDispatch) since
useMutationFeedback already handles loader and info dialog behavior; update the
import list at the top of CreateAlbumDialog.tsx to omit those three imports and
remove the const dispatch = useDispatch() declaration so ESLint warnings are
resolved while leaving useMutationFeedback and the component logic intact.
- Around line 152-158: The Label's htmlFor value ("hidden") doesn't match the
Switch id ("locked") in CreateAlbumDialog, so clicking the label won't toggle
the switch; update the Label htmlFor to "locked" (or change the Switch id to
"hidden") so they match, and ensure the underlying prop/state name (is_locked /
is_hidden) used in CreateAlbumDialog (and the similar case in EditAlbumDialog)
is consistent with the updated id to preserve accessibility and wiring to the
form control.

In `@frontend/src/components/Albums/EditAlbumDialog.tsx`:
- Around line 19-20: Remove unused imports and variable: delete showInfoDialog,
showLoader, hideLoader from the import list and remove the useDispatch import
and the local dispatch variable in EditAlbumDialog.tsx since useMutationFeedback
now handles loader/dialog dispatching; specifically remove references to the
symbols showInfoDialog, showLoader, hideLoader, useDispatch and the dispatch
variable to eliminate ESLint unused-import/variable warnings.
- Line 152: The Label's htmlFor ("hidden") doesn't match the Switch's id
("locked") and doesn't reflect the is_locked semantics; update them to match and
be descriptive (e.g., change Label htmlFor="hidden" to htmlFor="locked" or
rename both to "is_locked") so the <Label htmlFor="..."> pairs with <Switch
id="..."> and the form field (formData.is_locked / is_locked prop) consistently
uses that identifier; ensure any name/checked bindings for the Switch also use
the same key (is_locked or locked) throughout EditAlbumDialog.

In `@frontend/src/pages/Album/Album.tsx`:
- Around line 131-137: confirmDelete currently nulls out albumToDelete before
the async mutation completes, causing onSuccess handlers (from
useMutationFeedback) to see albumToDelete === null and fail to
dispatch(removeAlbum(...)). Fix by capturing the id (e.g., const id =
albumToDelete?.id) before calling deleteAlbumMutation.mutate(id) and use that id
in the mutation feedback/onSuccess to dispatch(removeAlbum(id)) and only clear
setAlbumToDelete(null) and close the dialog after onSuccess (or perform those
cleanup actions inside onSuccess) so the deleted album reference remains
available to the feedback callback.
- Around line 139-154: handleRefresh currently assumes refetch() throws on
failure; change it to await the refetch() result and check its properties (e.g.,
result.isError or result.error) instead of relying on catch — always call
dispatch(hideLoader()) after refetch returns, and if result.isError or
result.error is present call dispatch(showInfoDialog(...)) with the error
message/variant 'error'; update the function name handleRefresh and the refetch
call sites to use the returned result object from refetch().
🧹 Nitpick comments (6)
frontend/src/pages/Album/AlbumDetail.tsx (4)

131-152: Extensive as any casting bypasses type safety for backend data transformation.

Consider defining a typed interface for the backend album response shape (e.g., BackendAlbumResponse) and using it in place of the double as any cast. This would catch field renames or missing properties at compile time rather than silently producing undefined.


168-178: O(n×m) image filtering — use a Set for the lookup.

imageIds.includes(img.id) inside allImages.filter(...) is O(n×m). For large libraries this degrades quickly. Convert imageIds to a Set first.

Also, fetching the entire image library (fetchAllImages) just to hydrate album images is expensive. Consider a backend endpoint that returns full image objects for an album directly.

Quick fix for the Set lookup
-      const albumImages = allImages.filter((img) => imageIds.includes(img.id));
+      const imageIdSet = new Set(imageIds);
+      const albumImages = allImages.filter((img) => imageIdSet.has(img.id));

62-90: Sequential waterfall: album → images & allImages queries are chained via enabled: !!album.

Both the album-images and all-images queries depend on album being set in Redux (via the first useEffect), which only runs after the album query resolves. This creates a request waterfall (album fetch → render → effect → re-render → images fetch). If you already have albumId from the URL, you could enable the images queries with just !!albumId, removing the dependency on album being fully hydrated in Redux.


144-147: Fallback timestamps (new Date().toISOString()) are misleading.

If the backend doesn't return created_at/updated_at, fabricating a current timestamp silently hides missing data and may cause incorrect sorting or display. Consider using null / undefined with an appropriate type to surface the absence.

frontend/src/pages/Album/Album.tsx (1)

88-103: Loader persists during initial render when albums is empty.

While the useEffect dispatches showLoader during isLoading, the component body still renders. If albums in Redux starts as [], <EmptyAlbumsState /> briefly flashes behind the loader overlay. Consider a guard (e.g., if (isLoading) return null;) or checking isLoading before rendering the album grid.

frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (1)

154-158: prefers-color-scheme may not match the app's active theme.

If the app uses its own theme toggle (e.g., a dark/light class on <html>), window.matchMedia('(prefers-color-scheme: dark)') reflects the OS preference, not the app state. Consider checking the same source of truth the rest of the app uses (e.g., a dark class on document.documentElement).

Comment thread frontend/src/components/Albums/AddImagesToAlbumDialog.tsx Outdated
Comment thread frontend/src/components/Albums/CreateAlbumDialog.tsx Outdated
Comment thread frontend/src/components/Albums/CreateAlbumDialog.tsx Outdated
Comment thread frontend/src/components/Albums/EditAlbumDialog.tsx Outdated
Comment thread frontend/src/components/Albums/EditAlbumDialog.tsx
Comment thread frontend/src/pages/Album/Album.tsx
Comment thread frontend/src/pages/Album/Album.tsx

@rahulharpal1603 rahulharpal1603 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

The above error is occurring whenever I try to create an album or add images to the album.

Comment thread backend/app/database/albums.py Outdated
Comment on lines +30 to +49
def db_migrate_add_cover_image_column() -> None:
"""Add cover_image_path column to existing albums table if it doesn't exist"""
conn = None
try:
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()

# Check if column exists
cursor.execute("PRAGMA table_info(albums)")
columns = [column[1] for column in cursor.fetchall()]

if "cover_image_path" not in columns:
cursor.execute("ALTER TABLE albums ADD COLUMN cover_image_path TEXT")
conn.commit()
print("Added cover_image_path column to albums table")
finally:
if conn is not None:
conn.close()


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove migration logic. Not needed for now.

@SiddharthJiyani

Copy link
Copy Markdown
Contributor Author
Image The above error is occurring whenever I try to create an album or add images to the album.

Done, please check.

@rohan-pandeyy

Copy link
Copy Markdown
Member

Hi @SiddharthJiyani, could we catch up regarding the merge of this PR over a meeting call in AOSSIE's Discord Server Meeting Room?

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ This PR has merge conflicts.

Please resolve the merge conflicts before review.

Your PR will only be reviewed by a maintainer after all conflicts have been resolved.

📺 Watch this video to understand why conflicts occur and how to resolve them:
https://www.youtube.com/watch?v=Sqsz1-o7nXk

@rohan-pandeyy rohan-pandeyy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's follow the say UI for "Back" button as seen in AI tagging page when we open a particular face collection

Currently, the back button in Albums looks like this:

Image

So lets make this consistent with the existing Back button from face collection:

Image

Additionally, when you work on this change, please try and make the final UI somewhat like this:

Image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants